////////////////////////////////////////////////////////////////////////////////
//  Copyright 2011 Adam Kidd
//  
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//  
//    http://www.apache.org/licenses/LICENSE-2.0
//  
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
////////////////////////////////////////////////////////////////////////////////

package com.arcadefoundry.rest.http.client
{	
	import com.arcadefoundry.rest.http.HTTPRequestMethod;
	import com.arcadefoundry.rest.http.IHTTPRequest;
	import com.arcadefoundry.rest.http.events.HTTPSocketEvent;
	import com.arcadefoundry.rest.io.ByteBuffer;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.OutputProgressEvent;
	import flash.events.ProgressEvent;
	import flash.net.Socket;
	import flash.utils.ByteArray;

	internal class HTTPSocket extends EventDispatcher
	{
		//------------------------------------------------
		//
		// Properties
		//
		//------------------------------------------------
		/**
		 * Carriage-Return-Line-Feed - The separator used between headers and
		 * between headers and the content of an HTTP request/response. 
		 */
		private static const CRLF:String = "\r\n";
		
		/**
		 * The socket used for communication 
		 */
		private var socket:Socket;
		/**
		 * The targeted HTTP server port. 
		 */		
		private var port:int;
		/**
		 * The targeted HTTP server host URI. 
		 */		
		private var host:String;
		/**
		 * Buffer used for sending data in chunks. 
		 */		
		private var buffer:ByteBuffer;
		/**
		 * The request currently being processed. 
		 */
		private var request:IHTTPRequest;
		
		//------------------------------------------------
		//
		// Constructor
		//
		//------------------------------------------------
		/**
		 * The <code>HTTPSocket</code> is used for communicating with HTTP web
		 * servers.
		 *  
		 * @param host The URI of the targeted HTTP server.
		 * @param port The port of the targeted HTTP server. Defaults to 80.
		 * 
		 */
		public function HTTPSocket(host:String, port:int = 80)
		{
			this.host = host;
			this.port = port;
		}
		
		//------------------------------------------------
		//
		// Methods
		//
		//------------------------------------------------
		/**
		 * Sends a request over HTTP.
		 *  
		 * @param request The request that will be sent.
		 * 
		 */
		public function send(request:IHTTPRequest):void
		{
			this.request = request;
				
			open();
		}
		
		/**
		 * Closes the socket - used to cancel a request if necessary or for
		 * cleanup. 
		 * 
		 */
		public function close():void
		{
			if (socket) 
			{
				socket.close();
			}
		}
		
		/**
		 * Opens a new socket and connects to the HTTP server. 
		 * 
		 */
		private function open():void
		{
			// TODO: Re-use the socket?
			socket = new Socket();
			
			// Register event listeners
			socket.addEventListener(Event.CLOSE, onClose);
			socket.addEventListener(Event.CONNECT, onConnect);
			socket.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
			// OutputProgressEvents were added in AIR 3 & Flash 11: Used for determining when request operations are complete.
			socket.addEventListener(OutputProgressEvent.OUTPUT_PROGRESS, onOutputProgress);
			socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
			
			socket.connect(host, port);
		}
		
		/**
		 * Starts sending the request after the socket is connected.
		 *  
		 * @param event 
		 * 
		 */
		private function onConnect(event:Event):void
		{
			// TODO: Dispatch a connect event?
			var bytes:ByteArray = constructRequest(this.request);
			sendRequest(bytes);
		}
		
		
		/**
		 * Creates a ByteArray representation of an IHTTPRequest for writing to
		 * the socket.
		 *  
		 * @param request The IHTTPRequest to convert.
		 * @return A ByteArray containing a correctly formatted HTTP request.
		 * 
		 */
		private function constructRequest(request:IHTTPRequest):ByteArray
		{
			var bytes:ByteArray = new ByteArray();
			// Set the host header.
			request.headers.addHeader("Host",this.host);
			
			// Write the header first.
			bytes.writeUTFBytes(request.method + " " + request.uri + " " + "HTTP/1.1" + CRLF);
			bytes.writeUTFBytes(request.headers.content);
			// Insert additional CRLF after headers [Denotes separation between headers and content]
			bytes.writeUTFBytes(CRLF);
			
			// Only send the body if the request method is PUT or POST.
			if (request.method == HTTPRequestMethod.POST || request.method == HTTPRequestMethod.PUT)
			{
				if (request.body)
				{
					bytes.writeUTFBytes(request.body);
				}
			}
			// TODO: Remove debug code.
			// DEBUG: Trace output
			bytes.position = 0;
			trace(bytes.readUTFBytes(bytes.bytesAvailable));
			
			// Reset cursor
			bytes.position = 0;
			
			return bytes;
		}
		
		/**
		 * Writes the request to the socket in chunks.
		 *  
		 * @param bytes A ByteArray of the data to be sent.
		 * 
		 */
		private function sendRequest(bytes:ByteArray):void
		{
			// Use a buffer to send the request in chunks.
			buffer = new ByteBuffer(bytes);
			var initialChunk:ByteArray = buffer.read();
			socket.writeBytes(initialChunk);
			socket.flush();
		}
		
		/**
		 * Continues sending chunks of data to the server until the whole
		 * request has been sent.
		 *  
		 * @param event
		 * 
		 */
		private function onOutputProgress(event:OutputProgressEvent):void
		{
			if (event.bytesPending == 0)
			{
				if (buffer.bytesAvailable > 0)
				{
					socket.writeBytes(buffer.read());
					socket.flush();
				}
				else
				{
					// TODO: Dispatch a send complete event?
					// Finished sending!
					trace("Finished sending!");
				}
			}
		}
		
		/**
		 * Informs the client that the communication has ended, indicating the
		 * success or failure of the request operation.
		 * 
		 */
		private function onClose(event:Event):void
		{
			trace("Socket closed");
			// TODO: If all data received, dispatch a complete event.
			// Otherwise, dispatch an error.
		}
		
		/**
		 * Informs the client if there is an error during communication.
		 * 
		 */
		private function onIOError(event:IOErrorEvent):void
		{
			trace("IO Error: " + event.text);
			// TODO: Dispatch an error.
		}
		
		/**
		 * Handles the response from the HTTP server and passes it back to the client. 
		 * 
		 */
		private function onSocketData(event:ProgressEvent):void
		{
			// TODO: Parse incoming data to find when transfer is finished and close the socket.
			// TODO: Create HTTP Response from the data and dispatch an event containing the response for use by the client.
			
			// Temporary dispatcher to display received data in test client.
			dispatchEvent(new HTTPSocketEvent(HTTPSocketEvent.DATA_RECEIVED, socket.readUTFBytes(socket.bytesAvailable)));
		}
	}
}
